分类
联系方式
  1. 新浪微博
  2. E-mail

Dart eval:eval 方法

介绍

在《Dart eval:动态执行 Dart 代码》中的 demo 中,通过 dart_eval 的 eval 方法动态执行 Dart 代码。eval 内部是如何实现的?

注释

/// Evaluate the Dart [source] code. If the source is a raw expression such as "2 + 2" it will be evaluated directly
/// and the result will be returned; otherwise, the function [function] will be called with arguments specified
/// by [args]. You can use [plugins] to configure bridge classes.
/// You can also specify [outputFile] to write the generated EVC bytecode to a file.

执行 Dart 源代码。如果源码是一个原始表达式,比如 "2+2",会直接求值并返回对应结果。否则,会根据入参调用指定函数。你可以使用 Plugins 来配置桥接类。你也可以指定一个 outputFile,将生成的 EVC 代码写入到文件中。

方法签名

方法签名如下:

dynamic eval(String source,
    {String function = 'main',
    List args = const [],
    List<EvalPlugin> plugins = const [],
    @Deprecated('Use plugins instead') Function(Compiler)? compilerSettings,
    @Deprecated('Use plugins instead') Function(Runtime)? runtimeSettings,
    String? outputFile}) {

忽略废弃参数:

  • source:Dart 源代码
  • function:调用函数
  • args:传入参数
  • plugins:桥接插件
  • outputFile:EVC 代码生成路径

初始化编译器

通过如下代码初始化编译器,并将编译器传入各个插件实现插件挂载:

  final compiler = Compiler();
  for (final plugin in plugins) {
    plugin.configureForCompile(compiler);
  }

初始化代码变量

入参 source 在内部会进行调整,创建一个 _source 变量进行承接:

var _source = source;

代码变换

根据一系列逻辑对代码进行变换:

  if (!RegExp(r'(?:\w* )?' + function + r'\s?\([\s\S]*?\)\s?{').hasMatch(_source)) {
    if (!_source.contains(';')) {
      _source = '$_source;';
      if (!_source.contains('return')) {
        _source = 'return $_source';
      }
    }
    _source = 'dynamic $function() {$_source}';
  }

正则理解(借助 ChatGPT 帮我理解了这段正则):

  • 概括:检测代码中是否包括函数声明,其中 function 入参被拼接到字符串中
    • 能够匹配
      • 一些字符串 foo(a, b) {
      • 一些字符串(a, b) {
  • 具体解释:
    • r'(?:\w* )?':、
      • 括号内:
        • (?:) 表示一个非捕获组,它们在匹配时不会创建一个捕获组。在这里,非捕获组用于分组,但是不需要捕获组的值。
        • 然后是一个 \w*,它匹配零个或多个字母、数字或下划线。
        • 最后是一个空格,表示如果有函数名,那么它后面必须有一个空格。
      • 括号外:
        • 整个正则表达式中的问号(?)表示这个部分是可选的,即函数可能有名字,也可能没有名字。
    • r'\s?\([\s\S]*?\)\s?{'
      • \s 表示空格,首先是一个可选的空格
      • 然后是一个左圆括号(\(
      • 接下来是对任意字符,包括空格和换行符([\s\S])的贪婪量词(*),表示匹配零个或多个字符
      • 量词后面的问号(?)表示使用非贪婪模式,这意味着它会尽可能少地匹配字符。
      • 最后是一个右圆括号,然后又是一个可选的空格,最后是一个左大括号。

通过上述正则,判断代码中是否有 function 对应的函数签名:

  • 如果有:
    • 代码中是否有分号(;
      • 没有分号:说明代码中不包含语句,_source 不做处理
      • 没有分号:说明代码中包含语句,_source 做如下拼接:_source = 'return $_source';
    • 问题:这里为什么要做这种判断?
  • 最后将代码封装到一个给定函数里:_source = 'dynamic $function() {$_source}';

_source 实际值验证

上面 _source 的逻辑有点没理解,找几个实际例子验证一下:

"2 + 2":

dynamic main() {return 2 + 2;}

Cat('Fluffy') Demo:

class Cat {
  Cat(this.name);
  final String name;
  String speak() {
    return name;
  }
}
String main() {
  final cat = Cat('Fluffy');
  return cat.speak();
}

可以看到:

  • 2+2 中不包含函数声明,实际上直接进入 _source = 'dynamic $function() {$_source}';
  • 第二个例子也没有命中上面的正则逻辑
    • 原因是我给出的代码中已经包含 main 语句了
    • 也就是说如果不包含会进入

了解规律后,使用下面代码执行:

    print(eval("""
      2+2
      """, function: "hello"));

实际生成的 _source 为:

dynamic hello() {return       2+2
      ;}

原来,上面那段正则的含义是:在表达式 + 函数名场景下,自动将表达式封装到对应函数中。